package com.example.rxohosaudio;

import io.reactivex.annotations.NonNull;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.audio.AudioCapturer;
import ohos.media.audio.AudioCapturerInfo;
import ohos.media.audio.AudioStreamInfo;
import ohos.media.audio.AudioStreamInfo.EncodingFormat;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

public final class StreamAudioRecorder {
    private static final String TAG = "StreamAudioRecorder";
    private HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x00201, TAG);

    public static final int DEFAULT_SAMPLE_RATE = 44100;
    static final int DEFAULT_BUFFER_SIZE = 2048;
    private final AtomicBoolean mIsRecording;
    private ExecutorService mExecutorService;

    private StreamAudioRecorder() {
        mIsRecording = new AtomicBoolean(false);
    }

    public static StreamAudioRecorder getInstance() {
        return StreamAudioRecorderHolder.INSTANCE;
    }

    public synchronized boolean start(final AudioDataCallback audioDataCallback) {
        return start(DEFAULT_SAMPLE_RATE, (AudioStreamInfo.ChannelMask.CHANNEL_IN_MONO).getValue(),
                (EncodingFormat.ENCODING_PCM_16BIT).getValue(), DEFAULT_BUFFER_SIZE, audioDataCallback);
    }

    /**
     * AudioFormat.CHANNEL_IN_MONO
     * AudioFormat.ENCODING_PCM_16BIT
     */
    public synchronized boolean start(final int sampleRate, final int channelConfig, final int audioFormat,
                                      final int bufferSize, @NonNull AudioDataCallback audioDataCallback) {
        stop();

        mExecutorService = Executors.newSingleThreadExecutor();
        if (mIsRecording.compareAndSet(false, true)) {
            mExecutorService.execute(
                    new AudioCapturerRunnable(sampleRate, channelConfig, audioFormat, bufferSize,
                            audioDataCallback));
            return true;
        }
        return false;
    }

    public synchronized void stop() {
        mIsRecording.compareAndSet(true, false);

        if (mExecutorService != null) {
            mExecutorService.shutdown();
            mExecutorService = null;
        }
    }

    /**
     * Although frameworks jni implementation are the same for ENCODING_PCM_16BIT and
     * ENCODING_PCM_8BIT, the Java doc declared that the buffer type should be the corresponding
     * type, so we use different ways.
     */
    public interface AudioDataCallback {
        void onAudioData(byte[] data, int size);
        void onError();
    }

    private static final class StreamAudioRecorderHolder {
        private static final StreamAudioRecorder INSTANCE = new StreamAudioRecorder();
    }

    private class AudioCapturerRunnable implements Runnable {

        private AudioCapturer mAudioCapturer;
        private final AudioDataCallback mAudioDataCallback;

        private final byte[] mByteBuffer;
        private final short[] mShortBuffer;
        private final int mByteBufferSize;
        private final int mShortBufferSize;
        private final int mEncodingFormat;

        AudioCapturerRunnable(final int sampleRate, final int channelConfig, final int audioFormat, final int byteBufferSize,
                              @NonNull final AudioDataCallback audioDataCallback) {
            mEncodingFormat = audioFormat;
            int minBufferSize =
                    AudioCapturer.getMinBufferSize(sampleRate, channelConfig, mEncodingFormat);
            mByteBufferSize = byteBufferSize;
            mShortBufferSize = mByteBufferSize / 2;
            mByteBuffer = new byte[mByteBufferSize];
            mShortBuffer = new short[mShortBufferSize];
            AudioStreamInfo.Builder builder1 = new AudioStreamInfo.Builder();
            builder1.sampleRate(DEFAULT_SAMPLE_RATE);
            builder1.channelMask(AudioStreamInfo.ChannelMask.CHANNEL_IN_STEREO);
            if (audioFormat == 16) {
                builder1.encodingFormat(EncodingFormat.ENCODING_PCM_16BIT);
            } else {
                //TODO:check the encoding is right?
                builder1.encodingFormat(EncodingFormat.ENCODING_DEFAULT);
            }
            AudioStreamInfo audioStreamInfo = builder1.build();

            AudioCapturerInfo.Builder builder = new AudioCapturerInfo.Builder();
            builder.audioInputSource(AudioCapturerInfo.AudioInputSource.AUDIO_INPUT_SOURCE_MIC)
                    .audioStreamInfo(audioStreamInfo);

            AudioCapturerInfo audioCapturerInfo = builder.build();
            mAudioCapturer = new AudioCapturer(audioCapturerInfo);
            mAudioDataCallback = audioDataCallback;
        }

        @Override
        public void run() {
            if (mAudioCapturer.getState() == AudioCapturer.State.STATE_INITIALIZED) {
                try {
                    mAudioCapturer.start();
                } catch (IllegalStateException e) {
                    HiLog.warn(label, "startCapurering fail: " + e.getMessage());
                    mAudioDataCallback.onError();
                    return;
                }
                while (mIsRecording.get()) {
                    int ret;
                    if (mEncodingFormat == (EncodingFormat.ENCODING_PCM_16BIT).getValue()) {
                        ret = mAudioCapturer.read(mShortBuffer, 0, mShortBufferSize);
                        if (ret > 0) {
                            mAudioDataCallback.onAudioData(
                                    short2byte(mShortBuffer, ret, mByteBuffer), ret * 2);
                        } else {
                            onError(ret);
                            break;
                        }
                    } else {
                        ret = mAudioCapturer.read(mByteBuffer, 0, mByteBufferSize);
                        if (ret > 0) {
                            mAudioDataCallback.onAudioData(mByteBuffer, ret);
                        } else {
                            onError(ret);
                            break;
                        }
                    }
                }
            }
            mAudioCapturer.release();
        }

        private byte[] short2byte(final short[] sData, final int size, final byte[] bData) {
            if (size > sData.length || size * 2 > bData.length) {
                HiLog.warn(label, "short2byte: too long short data array");
            }
            for (int i = 0; i < size; i++) {
                bData[i * 2] = (byte) (sData[i] & 0x00FF);
                bData[(i * 2) + 1] = (byte) (sData[i] >> 8);
            }
            return bData;
        }

        private void onError(final int errorCode) {
            if (errorCode == AudioCapturer.ERROR_INVALID_OPERATION) {
                HiLog.warn(label, "record fail: ERROR_INVALID_OPERATION");
                mAudioDataCallback.onError();
            } else if (errorCode == AudioCapturer.ERROR_BAD_VALUE) {
                HiLog.warn(label, "record fail: ERROR_BAD_VALUE");
                mAudioDataCallback.onError();
            }
        }
    }
}
